// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/*
 * Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
 * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
 * Copyright (C) 2008, 2009 Torch Mobile Inc. All rights reserved.
 *     (http://www.torchmobile.com/)
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1.  Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 * 2.  Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
 *     its contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "content/renderer/history_controller.h"

#include <utility>

#include "base/memory/ptr_util.h"
#include "content/common/navigation_params.h"
#include "content/common/site_isolation_policy.h"
#include "content/renderer/render_frame_impl.h"
#include "content/renderer/render_view_impl.h"
#include "third_party/WebKit/public/web/WebFrameLoadType.h"
#include "third_party/WebKit/public/web/WebLocalFrame.h"

using blink::WebCachePolicy;
using blink::WebFrame;
using blink::WebHistoryCommitType;
using blink::WebHistoryItem;
using blink::WebURLRequest;

namespace content {

HistoryController::HistoryController(RenderViewImpl* render_view)
    : render_view_(render_view)
{
    // We don't use HistoryController in OOPIF enabled modes.
    DCHECK(!SiteIsolationPolicy::UseSubframeNavigationEntries());
}

HistoryController::~HistoryController()
{
}

bool HistoryController::GoToEntry(
    blink::WebLocalFrame* main_frame,
    std::unique_ptr<HistoryEntry> target_entry,
    std::unique_ptr<NavigationParams> navigation_params,
    WebCachePolicy cache_policy)
{
    DCHECK(!main_frame->parent());
    HistoryFrameLoadVector same_document_loads;
    HistoryFrameLoadVector different_document_loads;

    set_provisional_entry(std::move(target_entry));
    navigation_params_ = std::move(navigation_params);

    if (current_entry_) {
        RecursiveGoToEntry(
            main_frame, same_document_loads, different_document_loads);
    }

    if (same_document_loads.empty() && different_document_loads.empty()) {
        // If we don't have any frames to navigate at this point, either
        // (1) there is no previous history entry to compare against, or
        // (2) we were unable to match any frames by name. In the first case,
        // doing a different document navigation to the root item is the only valid
        // thing to do. In the second case, we should have been able to find a
        // frame to navigate based on names if this were a same document
        // navigation, so we can safely assume this is the different document case.
        different_document_loads.push_back(
            std::make_pair(main_frame, provisional_entry_->root()));
    }

    bool has_main_frame_request = false;
    for (const auto& item : same_document_loads) {
        WebFrame* frame = item.first;
        RenderFrameImpl* render_frame = RenderFrameImpl::FromWebFrame(frame);
        if (!render_frame)
            continue;
        render_frame->SetPendingNavigationParams(
            base::MakeUnique<NavigationParams>(*navigation_params_.get()));
        WebURLRequest request = frame->toWebLocalFrame()->requestFromHistoryItem(
            item.second, cache_policy);
        frame->toWebLocalFrame()->load(
            request, blink::WebFrameLoadType::BackForward, item.second,
            blink::WebHistorySameDocumentLoad);
        if (frame == main_frame)
            has_main_frame_request = true;
    }
    for (const auto& item : different_document_loads) {
        WebFrame* frame = item.first;
        RenderFrameImpl* render_frame = RenderFrameImpl::FromWebFrame(frame);
        if (!render_frame)
            continue;
        render_frame->SetPendingNavigationParams(
            base::MakeUnique<NavigationParams>(*navigation_params_.get()));
        WebURLRequest request = frame->toWebLocalFrame()->requestFromHistoryItem(
            item.second, cache_policy);
        frame->toWebLocalFrame()->load(
            request, blink::WebFrameLoadType::BackForward, item.second,
            blink::WebHistoryDifferentDocumentLoad);
        if (frame == main_frame)
            has_main_frame_request = true;
    }

    return has_main_frame_request;
}

void HistoryController::RecursiveGoToEntry(
    WebFrame* frame,
    HistoryFrameLoadVector& same_document_loads,
    HistoryFrameLoadVector& different_document_loads)
{
    DCHECK(provisional_entry_);
    DCHECK(current_entry_);
    RenderFrameImpl* render_frame = RenderFrameImpl::FromWebFrame(frame);
    const WebHistoryItem& new_item = provisional_entry_->GetItemForFrame(render_frame);

    // Use the last committed history item for the frame rather than
    // current_entry_, since the latter may not accurately reflect which URL is
    // currently committed in the frame.  See https://crbug.com/612713#c12.
    const WebHistoryItem& old_item = render_frame->current_history_item();

    if (new_item.isNull())
        return;

    if (old_item.isNull() || new_item.itemSequenceNumber() != old_item.itemSequenceNumber()) {
        if (!old_item.isNull() && new_item.documentSequenceNumber() == old_item.documentSequenceNumber()) {
            same_document_loads.push_back(std::make_pair(frame, new_item));

            // Returning here (and omitting child frames which have also changed) is
            // wrong, but not returning here is worse. See the discussion in
            // NavigationControllerImpl::FindFramesToNavigate for more information.
            return;
        } else {
            different_document_loads.push_back(std::make_pair(frame, new_item));
            // For a different document, the subframes will be destroyed, so there's
            // no need to consider them.
            return;
        }
    }

    for (WebFrame* child = frame->firstChild(); child;
         child = child->nextSibling()) {
        RecursiveGoToEntry(child, same_document_loads, different_document_loads);
    }
}

void HistoryController::UpdateForInitialLoadInChildFrame(
    RenderFrameImpl* frame,
    const WebHistoryItem& item)
{
    DCHECK_NE(frame->GetWebFrame()->top(), frame->GetWebFrame());
    if (!current_entry_)
        return;
    if (HistoryEntry::HistoryNode* existing_node = current_entry_->GetHistoryNodeForFrame(frame)) {
        // Clear the children and any NavigationParams if this commit isn't for
        // the same item.  Otherwise we might have stale data after a redirect.
        if (existing_node->item().itemSequenceNumber() != item.itemSequenceNumber()) {
            existing_node->RemoveChildren();
            navigation_params_.reset();
        }
        existing_node->set_item(item);
        return;
    }
    RenderFrameImpl* parent = RenderFrameImpl::FromWebFrame(frame->GetWebFrame()->parent());
    if (!parent)
        return;
    if (HistoryEntry::HistoryNode* parent_history_node = current_entry_->GetHistoryNodeForFrame(parent)) {
        parent_history_node->AddChild(item);
    }
}

void HistoryController::UpdateForCommit(RenderFrameImpl* frame,
    const WebHistoryItem& item,
    WebHistoryCommitType commit_type,
    bool navigation_within_page)
{
    switch (commit_type) {
    case blink::WebBackForwardCommit:
        if (!provisional_entry_) {
            // The provisional entry may have been discarded due to a navigation in
            // a different frame.  For main frames, it is not safe to leave the
            // current_entry_ in place, which may have a cross-site page and will be
            // included in the PageState for this commit.  Replace it with a new
            // HistoryEntry corresponding to the commit, and clear any stale
            // NavigationParams which might point to the wrong entry.
            //
            // This will lack any subframe history items that were in the original
            // provisional entry, but we don't know what those were after discarding
            // it.  We'll load the default URL in those subframes instead.
            //
            // TODO(creis): It's also possible to get here for subframe commits.
            // We'll leave a stale current_entry_ in that case, but that only causes
            // an earlier URL to load in the subframe when leaving and coming back,
            // and only in rare cases.  It does not risk a URL spoof, unlike the
            // main frame case.  Since this bug is not present in the new
            // FrameNavigationEntry-based navigation path (https://crbug.com/236848)
            // we'll wait for that to fix the subframe case.
            if (frame->IsMainFrame()) {
                current_entry_.reset(new HistoryEntry(item));
                navigation_params_.reset();
            }

            return;
        }

        // If the current entry is null, this must be a main frame commit.
        DCHECK(current_entry_ || frame->IsMainFrame());

        // Commit the provisional entry, but only if it is a plausible transition.
        // Do not commit it if the navigation is in a subframe and the provisional
        // entry's main frame item does not match the current entry's main frame,
        // which can happen if multiple forward navigations occur.  In that case,
        // committing the provisional entry would corrupt it, leading to a URL
        // spoof.  See https://crbug.com/597322.  (Note that the race in this bug
        // does not affect main frame navigations, only navigations in subframes.)
        //
        // Note that we cannot compare the provisional entry against |item|, since
        // |item| may have redirected to a different URL and ISN.  We also cannot
        // compare against the main frame's URL, since that may have changed due
        // to a replaceState.  (Even origin can change on replaceState in certain
        // modes.)
        //
        // It would be safe to additionally check the ISNs of all parent frames
        // (and not just the root), but that is less critical because it won't
        // lead to a URL spoof.
        if (frame->IsMainFrame() || current_entry_->root().itemSequenceNumber() == provisional_entry_->root().itemSequenceNumber()) {
            current_entry_ = std::move(provisional_entry_);
        }

        // We're guaranteed to have a current entry now.
        DCHECK(current_entry_);

        if (HistoryEntry::HistoryNode* node = current_entry_->GetHistoryNodeForFrame(frame)) {
            // Clear the children and any NavigationParams if this commit isn't for
            // the same item.  Otherwise we might have stale data from a race.
            if (node->item().itemSequenceNumber() != item.itemSequenceNumber()) {
                node->RemoveChildren();
                navigation_params_.reset();
            }

            node->set_item(item);
        }
        break;
    case blink::WebStandardCommit:
        CreateNewBackForwardItem(frame, item, navigation_within_page);
        break;
    case blink::WebInitialCommitInChildFrame:
        UpdateForInitialLoadInChildFrame(frame, item);
        break;
    case blink::WebHistoryInertCommit:
        // Even for inert commits (e.g., location.replace, client redirects), make
        // sure the current entry gets updated, if there is one.
        if (current_entry_) {
            if (HistoryEntry::HistoryNode* node = current_entry_->GetHistoryNodeForFrame(frame)) {
                // Inert commits that reset the page without changing the item (e.g.,
                // reloads, location.replace) shouldn't keep the old subtree.
                if (!navigation_within_page)
                    node->RemoveChildren();
                node->set_item(item);
            }
        }
        break;
    default:
        NOTREACHED() << "Invalid commit type: " << commit_type;
    }
}

HistoryEntry* HistoryController::GetCurrentEntry()
{
    return current_entry_.get();
}

WebHistoryItem HistoryController::GetItemForNewChildFrame(
    RenderFrameImpl* frame) const
{
    if (navigation_params_.get()) {
        frame->SetPendingNavigationParams(
            base::MakeUnique<NavigationParams>(*navigation_params_.get()));
    }

    if (!current_entry_)
        return WebHistoryItem();
    return current_entry_->GetItemForFrame(frame);
}

void HistoryController::RemoveChildrenForRedirect(RenderFrameImpl* frame)
{
    if (!provisional_entry_)
        return;
    if (HistoryEntry::HistoryNode* node = provisional_entry_->GetHistoryNodeForFrame(frame))
        node->RemoveChildren();
}

void HistoryController::CreateNewBackForwardItem(
    RenderFrameImpl* target_frame,
    const WebHistoryItem& new_item,
    bool clone_children_of_target)
{
    if (!current_entry_) {
        current_entry_.reset(new HistoryEntry(new_item));
    } else {
        current_entry_.reset(current_entry_->CloneAndReplace(
            new_item, clone_children_of_target, target_frame, render_view_));
    }
}

} // namespace content
